using System;
using System.ServiceModel;
using System.Diagnostics;
using System.Collections.Generic;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Net;
using System.Linq;
using System.Net.Security;
using System.Threading;
using WcfContrib.Resources;

namespace WcfContrib.Metadata
{
    /// <summary>
    /// Represents a helper for dealing with metadata
    /// </summary>
    public static class MetadataHelper
    {
        const int MessageSizeMultiplier = 5;
        const int MaximumResolvedReferences = 100;
        const int DefaultTimeoutInMillisecond = 1000 * 15;

        /// <summary>
        /// Gets the metadata description from a given address.
        /// Supports MEX or Http-Get and net.tcp, net.pipe, http or https protocols
        /// </summary>
        /// <param name="address">The metadata address.</param>
        /// <returns>The meta data description</returns>
        public static MetadataDescription GetMetadataDescription(string address)
        {
            return GetMetadataDescription(address, CredentialCache.DefaultCredentials, DefaultTimeoutInMillisecond);
        }

        /// <summary>
        /// Gets the metadata description from a given address.
        /// Supports MEX or Http-Get and net.tcp, net.pipe, http or https protocols
        /// </summary>
        /// <param name="address">The metadata address.</param>
        /// <param name="credentials">The credentials.</param>
        /// <param name="timeoutInMilliseconds">The timeout in milliseconds.</param>
        /// <returns>The meta data description</returns>
        public static MetadataDescription GetMetadataDescription(string address, ICredentials credentials, int timeoutInMilliseconds)
        {
            MetadataDescription description = new MetadataDescription();

            if (String.IsNullOrEmpty(address))
            {
                Debug.Assert(false, "Empty address");
                return null;
            }

            Uri addressUri = new Uri(address);

            if (addressUri.Scheme == "http")
            {
                HttpTransportBindingElement httpBindingElement = new HttpTransportBindingElement();
                httpBindingElement.MaxReceivedMessageSize *= MessageSizeMultiplier;

                //Try the HTTP MEX Endpoint
                try
                {
                    PopulateMetadataDescription(address, httpBindingElement, description, credentials, timeoutInMilliseconds);
                }
                catch
                { }

                //Try over HTTP-GET
                if (description.Endpoints == null)
                {
                    string httpGetAddress = address;
                    if (address.EndsWith("?wsdl") == false)
                    {
                        httpGetAddress += "?wsdl";
                    }
                    CustomBinding binding = new CustomBinding(httpBindingElement);
                    MetadataExchangeClient MEXClient = GetMexClient(binding, credentials, timeoutInMilliseconds);

                    PopulateMetadataDescription(MEXClient, httpGetAddress, description, MetadataExchangeClientMode.HttpGet, true, timeoutInMilliseconds);
                }
            }
            else if (addressUri.Scheme == "https")
            {
                HttpsTransportBindingElement httpsBindingElement = new HttpsTransportBindingElement();
                httpsBindingElement.MaxReceivedMessageSize *= MessageSizeMultiplier;

                //Try the HTTPS MEX Endpoint
                try
                {
                    PopulateMetadataDescription(address, httpsBindingElement, description, credentials, timeoutInMilliseconds);
                }
                catch
                { }

                //Try over HTTP-GET
                if (description.Endpoints == null)
                {
                    string httpsGetAddress = address;
                    if (address.EndsWith("?wsdl") == false)
                    {
                        httpsGetAddress += "?wsdl";
                    }
                    CustomBinding binding = new CustomBinding(httpsBindingElement);
                    MetadataExchangeClient MEXClient = GetMexClient(binding, credentials, timeoutInMilliseconds);

                    PopulateMetadataDescription(MEXClient, httpsGetAddress, description, MetadataExchangeClientMode.HttpGet, true, timeoutInMilliseconds);
                }
            }
            else if (addressUri.Scheme == "net.tcp")
            {
                TcpTransportBindingElement tcpBindingElement = new TcpTransportBindingElement();
                tcpBindingElement.MaxReceivedMessageSize *= MessageSizeMultiplier;
                PopulateMetadataDescription(address, tcpBindingElement, description, credentials, timeoutInMilliseconds);
            }
            else if (addressUri.Scheme == "net.pipe")
            {
                NamedPipeTransportBindingElement ipcBindingElement = new NamedPipeTransportBindingElement();
                ipcBindingElement.MaxReceivedMessageSize *= MessageSizeMultiplier;
                PopulateMetadataDescription(address, ipcBindingElement, description, credentials, timeoutInMilliseconds);
            }
            else
            {
                return null;
            }

            return description;
        }

        static MetadataExchangeClient GetMexClient(Binding binding, ICredentials credentials, int timeoutInMilliseconds)
        {
            MetadataExchangeClient MEXClient = new MetadataExchangeClient(binding);

            TimeSpan timeout = TimeSpan.FromMilliseconds(timeoutInMilliseconds);
            binding.OpenTimeout = timeout;
            binding.SendTimeout = timeout;
            MEXClient.OperationTimeout = timeout;

            NetworkCredential netCred = credentials as NetworkCredential;
            if (netCred != null)
            {
                MEXClient.SoapCredentials.Windows.ClientCredential = netCred;
            }
            
            MEXClient.SoapCredentials.ServiceCertificate.Authentication.RevocationMode = System.Security.Cryptography.X509Certificates.X509RevocationMode.NoCheck;
            MEXClient.SoapCredentials.ServiceCertificate.Authentication.CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None;

            MEXClient.HttpCredentials = credentials;
            MEXClient.MaximumResolvedReferences = MaximumResolvedReferences;

            return MEXClient;
        }

        static void PopulateMetadataDescription(string mexAddress, BindingElement bindingElement, MetadataDescription description, ICredentials credentials, int timeoutInMilliseconds)
        {
            CustomBinding binding = new CustomBinding(bindingElement);

            MetadataExchangeClient MEXClient = GetMexClient(binding, credentials, timeoutInMilliseconds);

            PopulateMetadataDescription(MEXClient, mexAddress, description, null, false, timeoutInMilliseconds);
        }

        static MetadataDescription PopulateMetadataDescription(MetadataExchangeClient mexClient, string address, MetadataDescription description, MetadataExchangeClientMode? mode, bool setMode, int timeoutInMilliseconds)
        {
            MetadataSet metadata = null;

            RemoteCertificateValidationCallback validDel = delegate { return true; };

            ServicePointManager.ServerCertificateValidationCallback += validDel;

            ManualResetEvent asyncWait = new ManualResetEvent(false);
            if (setMode && mode.HasValue)
            {
                mexClient.BeginGetMetadata(new Uri(address), mode.Value,
                    state =>
                    {
                        try
                        {
                            metadata = mexClient.EndGetMetadata(state);
                        }
                        catch (InvalidOperationException) { }
                        finally
                        {
                            try
                            {
                                asyncWait.Set();
                            }
                            catch (ObjectDisposedException) { }
                        }
                    }, null);
            }
            else
            {
                mexClient.BeginGetMetadata(new EndpointAddress(address),
                    state =>
                    {
                        try
                        {
                            metadata = mexClient.EndGetMetadata(state);
                        }
                        catch (InvalidOperationException) { }
                        finally
                        {
                            try
                            {
                                asyncWait.Set();
                            }
                            catch (ObjectDisposedException) { }
                        }
                    }, null);
            }

            asyncWait.WaitOne(timeoutInMilliseconds);
            asyncWait.Close();

            ServicePointManager.ServerCertificateValidationCallback -= validDel;

            if (metadata != null)
            {
                MetadataImporter importer = new WsdlImporter(metadata);
                PolicyTagger.TagImporter(importer);

                description.MexClient = mexClient;
                description.MetadataSet = metadata;
                description.MetadataImporter = importer;

                description.PopulateEndpoints(importer.ImportAllEndpoints());
            }
            else
            {
                throw GetMetadataCommunicationErrorException();
            }

            return description;
        }

        static Exception GetMetadataCommunicationErrorException()
        {
            return new CommunicationException(ExceptionMessages.Metadata_ConnectionError);
        }

        /// <summary>
        /// Determines whether a service metadata is available.
        /// </summary>
        /// <param name="mexAddress">The metadata address.</param>
        /// <returns>
        /// 	<c>true</c> if is the service metadata is available; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsMetadataAvailable(string mexAddress)
        {
            return IsMetadataAvailable(mexAddress, CredentialCache.DefaultCredentials, DefaultTimeoutInMillisecond);
        }

        /// <summary>
        /// Determines whether a service metadata is available.
        /// </summary>
        /// <param name="mexAddress">The metadata address.</param>
        /// <param name="credentials">The credentials.</param>
        /// <param name="timeoutInMilliseconds">The timeout in milliseconds.</param>
        /// <returns>
        /// 	<c>true</c> if is the service metadata is available; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsMetadataAvailable(string mexAddress, ICredentials credentials, int timeoutInMilliseconds)
        {
            try
            {
                MetadataHelper.GetMetadataDescription(mexAddress, credentials, timeoutInMilliseconds);
                return true;
            }
            catch (CommunicationException) { return false; }
        }

        static void PopulateMetadataDescription(string address, BindingElement bindingElement, MetadataDescription description)
        {
            CustomBinding binding = new CustomBinding(bindingElement);

            MetadataExchangeClient MEXClient = new MetadataExchangeClient(binding);
            MEXClient.MaximumResolvedReferences = MaximumResolvedReferences;
            MetadataSet metadata = MEXClient.GetMetadata(new EndpointAddress(address));
            MetadataImporter importer = new WsdlImporter(metadata);
            PolicyTagger.TagImporter(importer);

            description.MexClient = MEXClient;
            description.MetadataSet = metadata;
            description.MetadataImporter = importer;

            description.PopulateEndpoints(importer.ImportAllEndpoints());
        }
    }
}
